package monitoring

import (
	"context"
	"fmt"

	"go.opentelemetry.io/otel/metric"
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"

	"github.com/smartcontractkit/chainlink-common/pkg/beholder"
	"github.com/smartcontractkit/chainlink-common/pkg/metrics"

	monutils "github.com/smartcontractkit/chainlink/v2/core/monitoring"
)

// em AKA "engine metrics" is to locally scope these instruments to avoid
// data races in testing
type EngineMetrics struct {
	registerTriggerFailureCounter            metric.Int64Counter
	triggerWorkflowStarterErrorCounter       metric.Int64Counter
	workflowsRunningGauge                    metric.Int64Gauge
	capabilityInvocationCounter              metric.Int64Counter
	capabilityFailureCounter                 metric.Int64Counter
	workflowRegisteredCounter                metric.Int64Counter
	workflowUnregisteredCounter              metric.Int64Counter
	workflowExecutionRateLimitGlobalCounter  metric.Int64Counter
	workflowExecutionRateLimitPerUserCounter metric.Int64Counter
	workflowLimitGlobalCounter               metric.Int64Counter
	workflowLimitPerOwnerCounter             metric.Int64Counter
	workflowExecutionLatencyGauge            metric.Int64Gauge // ms
	workflowStepErrorCounter                 metric.Int64Counter
	workflowInitializationCounter            metric.Int64Counter
	workflowTriggerEventErrorCounter         metric.Int64Counter
	workflowTriggerEventQueueFullCounter     metric.Int64Counter

	// Deprecated: use the gauge instead
	engineHeartbeatCounter metric.Int64Counter
	engineHeartbeatGauge   metric.Int64Gauge

	workflowCompletedDurationSeconds   metric.Int64Histogram
	workflowEarlyExitDurationSeconds   metric.Int64Histogram
	workflowErrorDurationSeconds       metric.Int64Histogram
	workflowTimeoutDurationSeconds     metric.Int64Histogram
	workflowStepDurationSeconds        metric.Int64Histogram
	capabilityExecutionDurationSeconds metric.Int64Histogram
	workflowMissingMeteringReport      metric.Int64Counter
	workflowMeteringMode               metric.Int64Gauge

	workflowExecutionFailedCounter    metric.Int64Counter
	workflowExecutionStartedCounter   metric.Int64Counter
	workflowExecutionSucceededCounter metric.Int64Counter

	getSecretsDuration metric.Int64Histogram
}

func InitMonitoringResources() (em *EngineMetrics, err error) {
	em = &EngineMetrics{}

	em.workflowExecutionRateLimitGlobalCounter, err = beholder.GetMeter().Int64Counter("platform_engine_execution_ratelimit_global")
	if err != nil {
		return nil, fmt.Errorf("failed to register execution rate limit global counter: %w", err)
	}

	em.workflowExecutionRateLimitPerUserCounter, err = beholder.GetMeter().Int64Counter("platform_engine_execution_ratelimit_peruser")
	if err != nil {
		return nil, fmt.Errorf("failed to register execution rate limit per user counter: %w", err)
	}

	em.workflowLimitGlobalCounter, err = beholder.GetMeter().Int64Counter("platform_engine_limit_global")
	if err != nil {
		return nil, fmt.Errorf("failed to register execution limit global counter: %w", err)
	}

	em.workflowLimitPerOwnerCounter, err = beholder.GetMeter().Int64Counter("platform_engine_limit_perowner")
	if err != nil {
		return nil, fmt.Errorf("failed to register execution limit per owner counter: %w", err)
	}

	em.registerTriggerFailureCounter, err = beholder.GetMeter().Int64Counter("platform_engine_registertrigger_failures")
	if err != nil {
		return nil, fmt.Errorf("failed to register trigger failure counter: %w", err)
	}

	em.triggerWorkflowStarterErrorCounter, err = beholder.GetMeter().Int64Counter("platform_engine_triggerworkflow_starter_errors")
	if err != nil {
		return nil, fmt.Errorf("failed to register trigger workflow starter error counter: %w", err)
	}

	em.workflowsRunningGauge, err = beholder.GetMeter().Int64Gauge("platform_engine_workflow_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflows running gauge: %w", err)
	}

	em.capabilityInvocationCounter, err = beholder.GetMeter().Int64Counter("platform_engine_capabilities_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register capability invocation counter: %w", err)
	}

	em.capabilityFailureCounter, err = beholder.GetMeter().Int64Counter("platform_engine_capabilities_failures")
	if err != nil {
		return nil, fmt.Errorf("failed to register capability failure counter: %w", err)
	}

	em.workflowRegisteredCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_registered_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow registered counter: %w", err)
	}

	em.workflowUnregisteredCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_unregistered_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow unregistered counter: %w", err)
	}

	em.workflowExecutionLatencyGauge, err = beholder.GetMeter().Int64Gauge(
		"platform_engine_workflow_time",
		metric.WithUnit("ms"))
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow execution latency gauge: %w", err)
	}

	em.workflowInitializationCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_initializations")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow initialization counter: %w", err)
	}

	em.workflowStepErrorCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_errors")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow step error counter: %w", err)
	}

	em.workflowTriggerEventErrorCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_trigger_event_errors")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow trigger event error counter: %w", err)
	}

	em.workflowTriggerEventQueueFullCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_trigger_event_queue_full")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow trigger event queue full counter: %w", err)
	}

	em.workflowExecutionStartedCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_execution_started_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow execution started counter: %w", err)
	}

	em.workflowExecutionFailedCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_execution_failed_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow execution failed counter: %w", err)
	}

	em.workflowExecutionSucceededCounter, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_execution_succeeded_count")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow execution succeeded counter: %w", err)
	}

	// Deprecated: use the gauge below
	em.engineHeartbeatCounter, err = beholder.GetMeter().Int64Counter("platform_engine_heartbeat")
	if err != nil {
		return nil, fmt.Errorf("failed to register engine heartbeat counter: %w", err)
	}

	em.engineHeartbeatGauge, err = beholder.GetMeter().Int64Gauge("platform_engine_workflow_heartbeat")
	if err != nil {
		return nil, fmt.Errorf("failed to register engine heartbeat gauge: %w", err)
	}

	em.workflowCompletedDurationSeconds, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_workflow_completed_time_seconds",
		metric.WithDescription("Distribution of completed execution latencies"),
		metric.WithUnit("seconds"))
	if err != nil {
		return nil, fmt.Errorf("failed to register completed duration histogram: %w", err)
	}

	em.workflowEarlyExitDurationSeconds, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_workflow_earlyexit_time_seconds",
		metric.WithDescription("Distribution of earlyexit execution latencies"),
		metric.WithUnit("seconds"))
	if err != nil {
		return nil, fmt.Errorf("failed to register early exit duration histogram: %w", err)
	}

	em.workflowErrorDurationSeconds, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_workflow_error_time_seconds",
		metric.WithDescription("Distribution of error execution latencies"),
		metric.WithUnit("seconds"))
	if err != nil {
		return nil, fmt.Errorf("failed to register error duration histogram: %w", err)
	}

	em.workflowTimeoutDurationSeconds, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_workflow_timeout_time_seconds",
		metric.WithDescription("Distribution of timeout execution latencies"),
		metric.WithUnit("seconds"))
	if err != nil {
		return nil, fmt.Errorf("failed to register timeout duration histogram: %w", err)
	}

	em.workflowStepDurationSeconds, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_workflow_step_time_seconds",
		metric.WithDescription("Distribution of step execution times"),
		metric.WithUnit("seconds"))
	if err != nil {
		return nil, fmt.Errorf("failed to register step execution time histogram: %w", err)
	}

	em.capabilityExecutionDurationSeconds, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_capability_execution_time_seconds",
		metric.WithDescription("Distribution of capability execution times"),
		metric.WithUnit("seconds"))
	if err != nil {
		return nil, fmt.Errorf("failed to register capability execution time histogram: %w", err)
	}

	em.workflowMissingMeteringReport, err = beholder.GetMeter().Int64Counter("platform_engine_workflow_missing_metering_report")
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow metering missing counter: %w", err)
	}

	em.workflowMeteringMode, err = beholder.GetMeter().Int64Gauge(
		"platform_engine_workflow_metering_mode",
		metric.WithUnit("active"))
	if err != nil {
		return nil, fmt.Errorf("failed to register workflow metering mode gauge: %w", err)
	}

	em.getSecretsDuration, err = beholder.GetMeter().Int64Histogram(
		"platform_engine_get_secrets_duration_ms",
		metric.WithDescription("Duration of GetSecrets calls in ms"),
		metric.WithUnit("ms"),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to create platform_engine_get_secrets_duration_ms metric: %w", err)
	}

	return em, nil
}

// Note: due to the OTEL specification, all histogram buckets
// Must be defined when the beholder client is created
func MetricViews() []sdkmetric.View {
	return []sdkmetric.View{
		sdkmetric.NewView(
			sdkmetric.Instrument{Name: "platform_engine_workflow_earlyexit_time_seconds"},
			sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
				Boundaries: []float64{0, 1, 10, 30, 120},
			}},
		),
		sdkmetric.NewView(
			sdkmetric.Instrument{Name: "platform_engine_workflow_completed_time_seconds"},
			sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
				// increased granularity for the workflow execution latencies near expected values
				Boundaries: []float64{0, 10, 20, 40, 50, 70, 90, 120, 150, 180, 210, 300, 600, 900, 1200},
			}},
		),
		sdkmetric.NewView(
			sdkmetric.Instrument{Name: "platform_engine_workflow_error_time_seconds"},
			sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
				Boundaries: []float64{0, 30, 60, 120, 240, 600},
			}},
		),
		sdkmetric.NewView(
			sdkmetric.Instrument{Name: "platform_engine_workflow_step_time_seconds"},
			sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
				Boundaries: []float64{0, 20, 60, 120, 240},
			}},
		),
		sdkmetric.NewView(
			sdkmetric.Instrument{Name: "platform_engine_capability_execution_time_seconds"},
			sdkmetric.Stream{Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
				Boundaries: []float64{0, 5, 10, 20, 60, 120, 240},
			}},
		),
	}
}

// WorkflowsMetricLabeler wraps monitoring.MetricsLabeler to provide workflow specific utilities
// for monitoring resources
type WorkflowsMetricLabeler struct {
	metrics.Labeler
	em *EngineMetrics
}

func NewWorkflowsMetricLabeler(labeler metrics.Labeler, em *EngineMetrics) *WorkflowsMetricLabeler {
	return &WorkflowsMetricLabeler{labeler, em}
}

func (c WorkflowsMetricLabeler) With(keyValues ...string) *WorkflowsMetricLabeler {
	return &WorkflowsMetricLabeler{c.Labeler.With(keyValues...), c.em}
}

func (c WorkflowsMetricLabeler) IncrementWorkflowExecutionRateLimitGlobalCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowExecutionRateLimitGlobalCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowExecutionRateLimitPerUserCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowExecutionRateLimitPerUserCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowLimitGlobalCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowLimitGlobalCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowLimitPerOwnerCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowLimitPerOwnerCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementRegisterTriggerFailureCounter(ctx context.Context) {
	otelLabels := monutils.KvMapToOtelAttributes(c.Labels)
	c.em.registerTriggerFailureCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementTriggerWorkflowStarterErrorCounter(ctx context.Context) {
	otelLabels := monutils.KvMapToOtelAttributes(c.Labels)
	c.em.triggerWorkflowStarterErrorCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementCapabilityInvocationCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.capabilityInvocationCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowExecutionLatencyGauge(ctx context.Context, val int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowExecutionLatencyGauge.Record(ctx, val, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementTotalWorkflowStepErrorsCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowStepErrorCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateTotalWorkflowsGauge(ctx context.Context, val int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowsRunningGauge.Record(ctx, val, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementEngineHeartbeatCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.engineHeartbeatCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) EngineHeartbeatGauge(ctx context.Context, val int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.engineHeartbeatGauge.Record(ctx, val, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementCapabilityFailureCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.capabilityFailureCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowRegisteredCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowRegisteredCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowUnregisteredCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowUnregisteredCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowInitializationCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowInitializationCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowTriggerEventErrorCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowTriggerEventErrorCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowTriggerEventQueueFullCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowTriggerEventQueueFullCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowCompletedDurationHistogram(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowCompletedDurationSeconds.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowEarlyExitDurationHistogram(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowEarlyExitDurationSeconds.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowErrorDurationHistogram(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowErrorDurationSeconds.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowTimeoutDurationHistogram(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowTimeoutDurationSeconds.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowStepDurationHistogram(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowStepDurationSeconds.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateCapabilityExecutionDurationHistogram(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.capabilityExecutionDurationSeconds.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowMissingMeteringReport(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowMissingMeteringReport.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) UpdateWorkflowMeteringModeGauge(ctx context.Context, on bool) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()

	var val int64
	if on {
		val = 1
	}

	c.em.workflowMeteringMode.Record(ctx, val, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) RecordGetSecretsDuration(ctx context.Context, duration int64) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.getSecretsDuration.Record(ctx, duration, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowExecutionFailedCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowExecutionFailedCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowExecutionSucceededCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowExecutionSucceededCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}

func (c WorkflowsMetricLabeler) IncrementWorkflowExecutionStartedCounter(ctx context.Context) {
	otelLabels := beholder.OtelAttributes(c.Labels).AsStringAttributes()
	c.em.workflowExecutionStartedCounter.Add(ctx, 1, metric.WithAttributes(otelLabels...))
}
